// TODO: CODE REVIEW & CLEANUP!
//
// SimpleSerialHardware.cs
//
// Implements the SimpleSerialHardware class.
//

using System;
using System.Diagnostics;
using System.Text;
#if !SILVERLIGHT
using System.IO.Ports;
#endif

namespace HomeUX.Utilities
{

/// <summary>
/// TODO
/// </summary>
/// <typeparam name="ReplyType">TODO</typeparam>
public class SimpleSerialHardware<ReplyType> : IDisposable where ReplyType : class
{
    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Private Fields
    //

    /// <summary>
    /// The serial port connected to the device hardware.
    /// </summary>
    VirtualSerialPort _serialPort;

    /// <summary>
    /// Specifies how <r>SimpleSerialHardware{}</r> communicates with the hardware.
    /// </summary>
    SimpleSerialHardwareMode _mode;

    /// <summary>
    /// If the last text received from the device hardware (over the serial port) was an incomplete
    /// reply (e.g. "MASTER=1,") and/or an incomplete notification (e.g. "!FAIL="), this is the
    /// text of that reply and/or notification.  Otherwise, <r>_partialReceived</r> is
    /// <n>String.Empty</n>.
    /// </summary>
    string _partialReceived = String.Empty;

    /// <summary>
    /// If not <n>null</n>, this is a command (without a terminating CR) that the driver sent to
    /// the hardware -- this class is currently waiting an echo of this command
    /// followed by a reply to this command -- both will be bufferred in
    /// <r>_partialReceived</r> until both are received.  If <r>_currentCommand</r>
    /// is <n>null</n>, no command currently is being executed.
    /// </summary>
    string _currentCommand;

    /// <summary>
    /// This work item executes if <r>_currentCommand</r> times out, i.e. if the hardware
    /// takes too long to reply.  If no command currently is being executed,
    /// <r>_currentCommandTimeout</r> is <n>null</n>.
    /// </summary>
    WorkItem _currentCommandTimeout;

    /// <summary>
    /// If the driver has sent a command <r>_currentCommand</r> to the hardware and is
    /// waiting for a reply, this is the method to call when a reply is received -- the method
    /// argument type is defined by the application, but will be <n>null</n> on failure.
    /// If no command currently is being executed, <r>_onReply</r> is <n>null</n>.
    /// </summary>
    Action<ReplyType> _onReply;

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Protected Properties
    //

    /// <summary>
    /// TODO
    /// </summary>
    protected WorkQueue WorkQueue { get; set; }


    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Public Properties
    //

    /// <summary>
    /// A short label describing the hardware.  For logging purposes.
    /// </summary>
    public string HardwareLabel { get; private set; }

    /// <summary>
    /// Incomplete text from the hardware that's longer than this is discarded.
    /// </summary>
    public int MaxPartialReceivedLength { get; set; }

    /// <summary>
    /// Commands time out after this interval.
    /// </summary>
    public TimeSpan CommandTimeoutInterval { get; set; }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Public Events
    //

    /// <summary>
    /// TODO
    /// </summary>
    public event EventHandler<SimpleSerialHardwareCommandTimedOutEventArgs> CommandTimedOut;

	///////////////////////////////////////////////////////////////////////////////////////////////
	// Public Methods
	//

    public SimpleSerialHardware(WorkQueue workQueue, VirtualSerialPort serialPort,
        SimpleSerialHardwareMode mode, string hardwareLabel)
    {
        // store state
        WorkQueue = workQueue;
        _serialPort = serialPort;
        _mode = mode;
        HardwareLabel = hardwareLabel;

        // set defaults
        MaxPartialReceivedLength = 100000;
        CommandTimeoutInterval = TimeSpan.FromSeconds(60);
    }

    /// <summary>
    /// TODO
    /// </summary>
    public void Open()
    {
        _serialPort.HandleEvents(WorkQueue, OnSerialPortBytesReceived, null);
        _serialPort.Open();
    }

    /// <summary>
    /// TODO
    /// </summary>
    /// <param name="command"></param>
    /// <param name="onReply"></param>
    public void TransmitCommand(string command, Action<ReplyType> onReply)
    {
        if ((_currentCommand != null) || (_onReply != null) ||
            (_currentCommandTimeout != null))
        {
            throw new InvalidOperationException(
                "TransmitCommand cannot be used if a command is currently executing");
        }

        _currentCommand = command;
        _onReply = onReply;

        bool lineMode = (_mode == SimpleSerialHardwareMode.Line);

		// log <command>
        if (Debugger.IsLogging())
        {
            string newlineLabel = lineMode ?
                _serialPort.NewLine.Replace("\r", "\\r").Replace("\n", "\\n") : String.Empty;
            Util.Trace("{0} <-- {1}{2}", HardwareLabel, command,
                newlineLabel);
        }

        if (lineMode)
            _serialPort.WriteLine(command);
        else
            _serialPort.Write(command);
        _currentCommandTimeout = WorkQueue.Add((int)CommandTimeoutInterval.TotalMilliseconds,
            OnCommandTimeout);
    }

    // reset the driver state to indicate that we're no longer waiting
    // for a reply from the hardware
    protected virtual void EndCommand()
    {
        _currentCommand = null;
        _onReply = null;
        CancelCommandTimeout();
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Protected Methods
    //

    /// <summary>
    /// TODO
    /// </summary>
    /// 
    /// <param name="received">TODO</param>
    /// 
    /// <param name="pushBack">TODO</param>
    /// 
    /// <param name="currentCommand">TODO</param>
    /// 
    /// <param name="onReply">TODO</param>
    /// 
    protected virtual void ParseReceivedString(string received, ref string pushBack,
        string currentCommand, Action<ReplyType> onReply)
    {
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Private Methods
    //

    //int _logItemCount;

    /// <summary>
    /// Called when data is received on the serial port connected to the device hardware. 
    /// </summary>
    ///
    void OnSerialPortBytesReceived()
    {
        // do nothing if the serial port was closed
        if (_serialPort == null)
            return;

        // set <received> to be the string received from the serial port
        //   -- The contents of <_partialHardwareReply> -- which contain previously-received
        //      characters that we couldn't process yet (because we're waiting for additional
        //      characters) -- are moved to the beginning of the received string.
        string received = _serialPort.ReadExisting();
        if (received.Length == 0)
            return;

        // log <received>
        if (Debugger.IsLogging())
        {
            //string label = String.Format("[{0}] {1}", ++_logItemCount, HardwareLabel);
            string formatted = received.Replace("\r\n", "\\r\\n\xFFFF").Replace("\r", "\\r\xFFFF")
                .Replace("\n", "\\n\xFFFF");
            StringBuilder output = new StringBuilder(formatted.Length * 2);
            bool isFirstLine = true;
            string labelBlanks = null;
            foreach (string line in formatted.Split('\xFFFF'))
            {
                if (!isFirstLine)
                    labelBlanks = new String(' ', HardwareLabel.Length);
                output.AppendFormat("{0} --> {1}\n",
                        (isFirstLine ? HardwareLabel : labelBlanks), line);
                isFirstLine = false;
            }
            Util.Trace(output.ToString().TrimEnd());
        }

        received = _partialReceived + received;
        _partialReceived = String.Empty;

        ParseReceivedString(received, ref _partialReceived, _currentCommand, _onReply);

        // check for out-of-control hardware sending (for example) a long stream of gibberish
        if (_partialReceived.Length > MaxPartialReceivedLength)
            _partialReceived = String.Empty;
    }


    void CancelCommandTimeout()
    {
        if (_currentCommandTimeout != null)
        {
            _currentCommandTimeout.Abort();
            _currentCommandTimeout = null;
        }
    }

    void OnCommandTimeout()
    {
        _currentCommandTimeout = null;

        // mitigate a possible out-of-sync condition by clearing <_partialReceived>
        _partialReceived = String.Empty;

        string currentCommand = _currentCommand;
        Action<ReplyType> onReply = _onReply;

        if (CommandTimedOut != null)
        {
            CommandTimedOut(this, new SimpleSerialHardwareCommandTimedOutEventArgs()
            {
                Command = currentCommand
            });
        }

        EndCommand();
        if (onReply != null)
            onReply(null);
    }

	///////////////////////////////////////////////////////////////////////////////////////////////
	// IDisposable Overrides
	//

    public void Dispose()
    {
        if (_serialPort != null)
        {
            _serialPort.Dispose();
            _serialPort = null;
        }
        CancelCommandTimeout();
    }
}

/// <summary>
/// TODO
/// </summary>
public class SimpleSerialHardwareCommandTimedOutEventArgs : EventArgs
{
    public string Command { get; set; }
};

/// <summary>
/// By convention, this is the type specified by the <n>ReplyType</n> type parameter of
/// <r>SimpleSerialHardware{}</r> (or class derived from <r>SimpleSerialHardware{}</r>) in the case
/// where the hardware doesn't reply to commands, or where the replies contain no specific
/// information (for example, the reply is just an echo of the command).
/// </summary>
///
public class GenericReply
{
    static GenericReply _reply = new GenericReply();

	/// <summary>
	/// A single, static instance of <r>GenericReply</r>.
	/// </summary>
    public static GenericReply Reply
	{
		get
		{
			return _reply;
		}
	}
}

/// <summary>
/// Specifies how <r>SimpleSerialHardware{}</r> communicates with the hardware.
/// </summary>
public enum SimpleSerialHardwareMode
{
    /// <summary>
    /// <r>VirtualSerialPort.Write</r>(String) (not <r>VirtualSerialPort.WriteLine</r>) is use
    /// when transmitting to the hardware.
    /// </summary>
	Character,

    /// <summary>
    /// <r>VirtualSerialPort.WriteLine</r> (not <r>VirtualSerialPort.Write</r>) is used when
    /// transmitting to the hardware.
    /// </summary>
    Line
}

}

